package com.hzz.mall.seckill.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.mq.SecKillOrderTo;
import com.atguigu.common.utils.R;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.hzz.mall.seckill.feign.CouponFeignService;
import com.hzz.mall.seckill.feign.ProductFeignService;
import com.hzz.mall.seckill.interceptor.LoginUserInterceptor;
import com.hzz.mall.seckill.service.SeckillService;
import com.hzz.mall.seckill.to.SecKillSkuRedisTo;
import com.hzz.mall.seckill.vo.SeckillSessionWithSkusVo;
import com.hzz.mall.seckill.vo.SeckillSkuRelationEntityVo;
import com.hzz.mall.seckill.vo.SkuInfoVo;
import com.hzz.mall.seckill.vo.UserInfo1;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    RabbitTemplate rabbitTemplate;

    private final String SESSION_CACHE_PREFIX="seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX="seckill:skus:";
    private final String SKU_SKUSTOCK_SEMAPHORE="seckill:stock:";
    @Override
    public void uploadSeckillSkuLast3day() {
        //扫描需要参加的秒杀活动
        R r = couponFeignService.getLate3DaySession();
        if (r.getCode()==0){
            List<SeckillSessionWithSkusVo> sessionData = r.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            //把商品信息放进redis缓存中
            //1.缓存活动信息
            saveSecKillSession(sessionData);// 会判断有没有加错
            //2.缓存商品信息
            saveSessionSkuInfo(sessionData);
        }
    }
    public List<SecKillSkuRedisTo> blockHandler(BlockException e) {
        log.error("getCurrentSeckillSkusResource被限流了");
        return null;
    }
    /**
     * 获取当前时间参与秒杀的商品信息
     * 想要查询当前商品的信息，1.couponFeignService远程服务，查询的活动是三天的信息，
     * 从当天00:00:00到后2天的23:59:59
     * 2.下面的这个方法是查询当前的时间大于活动的开始时间和小于结束信息 between 10 and 20 包含10和20这条记录
     *
     * blockHandler 函数会在原方法被限流降级，保护时调用，而fallback函数会针对所有的类型排序
     * @return
     */
    @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler ="blockHandler" )
    @Override
    public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
        // 1.确定当前时间属于那个秒杀场次
        //redis中的时间都是计算距离1970的时间
        long time = new Date().getTime();
        Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX + "*");
        for (String key : keys) {
            //seckill:sessions:1640448000000_1640455200000
            String replace = key.replace(SESSION_CACHE_PREFIX, "");
            String[] split = replace.split("_");
            long start = Long.parseLong(split[0]);
            long end = Long.parseLong(split[1]);
            if(time >= start && time <= end){
                // 2.获取这个秒杀场次的所有商品信息
                // 2.获取这个秒杀场次的所有商品信息
                List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                // multiGet 获取多个key对应的数据  range key的范围
                List<String> list = hashOps.multiGet(range);
                if(list != null){
                    return list.stream().map(item -> {
                        SecKillSkuRedisTo redisTo = JSON.parseObject(item, SecKillSkuRedisTo.class);
                        //redisTo.setRandomCode(null); 秒杀开始就需要随机码
                        return redisTo;
                    }).collect(Collectors.toList());
                }
                break;
            }
        }
        return null;
    }

    //获取某一个商品的秒杀信息
    @Override
    public SecKillSkuRedisTo getSeckillSkuInfo(Long skuId) {
        BoundHashOperations<String,String, String> ops =
                stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        //获取所有商品的hash key
        Set<String> keys = ops.keys();
        if (keys!=null&&keys.size()>0) {
            for (String key : keys) {
                //1-1
                //通过正则表达式匹配 数字-当前skuid的商品  key是用来匹配的
                if (Pattern.matches("\\d-" + skuId,key)) {
                    String v = ops.get(key);
                    SecKillSkuRedisTo redisTo = JSON.parseObject(v, SecKillSkuRedisTo.class);
                    //当前商品参与秒杀活动
                    if (redisTo!=null){
                        long current = System.currentTimeMillis();
                        //当前活动在有效期，暴露商品随机码返回
                        if (redisTo.getStartTime() < current && redisTo.getEndTime() > current) {
                            return redisTo;
                        }
                        //当前商品不再秒杀有效期，则隐藏秒杀所需的商品随机码
                        redisTo.setRandomCode(null);
                        return redisTo;
                    }
                }
            }
        }
        return null;
    }

    @Override
    public String kill(String killId, String key, Integer num) {
        //判断是否登录，拦截器已经做了  拿到当前用户的信息
        UserInfo1 userInfo1 = LoginUserInterceptor.threadLocal.get();

        // 1.获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if(StringUtils.isEmpty(json)){
           return null;
        }else{
            SecKillSkuRedisTo redisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
            // 校验合法性
            long time = new Date().getTime();   //时间的合法性
            if(time >= redisTo.getStartTime() && time <= redisTo.getEndTime()){
                //2.校验随机码和商品的id
                String randomCode = redisTo.getRandomCode();
                String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
                if(randomCode.equals(key) && killId.equals(skuId)){
                    //数据合法   每个人限购的数量1
                    BigDecimal limit = redisTo.getSeckillLimit();
                    if(num <= limit.intValue()){
                        //4.验证这个人是否已经购买过了(幂等性 只能买一次，如果只要秒杀成功，就去占位)
                        String redisKey = userInfo1.getUserId() + "-" + skuId;
                        // 让数据自动过期
                        long ttl = redisTo.getEndTime() - redisTo.getStartTime();
                        //设置自动过期时间，redis中没有这个key时才能占位成功 MILLISECONDS毫秒 没有将设置成功，下次就失败不执行了
                        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl<0?0:ttl, TimeUnit.MILLISECONDS);
                        if(aBoolean) {
                            // 占位成功 说明从来没买过// 前面的逻辑是acquireSemaphore
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_SKUSTOCK_SEMAPHORE + randomCode);
                            boolean acquire = semaphore.tryAcquire(num);// acquire阻塞  tryAcquire非阻塞
                            if(acquire) {
                                // 秒杀成功
                                // 快速下单 发送MQ
                                String orderSn = IdWorker.getTimeId() + UUID.randomUUID().toString().replace("-","").substring(7,8);
                                SecKillOrderTo orderTo = new  SecKillOrderTo();
                                orderTo.setOrderSn(orderSn);
                                orderTo.setMemberId(userInfo1.getUserId());
                                orderTo.setMemberName(userInfo1.getUserName());
                                orderTo.setNum(num);
                                orderTo.setSkuId(redisTo.getSkuId());
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo);
                                return orderSn;
                            }
                        }else {
                            //已经购买过了
                            return null;
                        }
                    }
                }else {
                    return null;
                }

            }else {
                return null;
            }

        }
        return null;
    }

    public void saveSecKillSession(List<SeckillSessionWithSkusVo> sessionData){
        if (sessionData!=null && sessionData.size()>0) {
            sessionData.stream().forEach(session -> {
                long startTime = session.getStartTime().getTime();
                long endTime = session.getEndTime().getTime();
                String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;

                Boolean hasKey = stringRedisTemplate.hasKey(key);
                // 防止重复添加活动到redis中   活动场次id:PromotionSessionId
                if (!hasKey) {
                    List<String> collect = session.getRelationSkus().stream()
                            .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                    stringRedisTemplate.opsForList().leftPushAll(key, collect);
                }
            });
        }
    }
    public void  saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions){
        if (sessions!=null){
            BoundHashOperations<String, Object, Object> ops =
                    stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); // "seckill:skus:";
            // 遍历session
            sessions.stream().forEach(session->{
                // 遍历活动商品信息
                session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                    SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                    // 1.商品的随机码
                    String randomCode = UUID.randomUUID().toString().replace("-", "");
                    // 缓存中没有再添加
                    if(!ops.hasKey(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId())) {
                        //缓存商品
                        //2. sku的基本信息 远程调用product服务
                        Long skuId = seckillSkuVo.getSkuId();
                        R info = productFeignService.getSkuInfo(skuId);
                        if (info.getCode() == 0) {
                            SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                            });
                            redisTo.setSkuInfoVo(skuInfo);
                        }
                        //3.秒杀信息
                        BeanUtils.copyProperties(seckillSkuVo, redisTo);

                        // 4.设置当前商品的秒杀信息时间
                        redisTo.setStartTime(session.getStartTime().getTime());
                        redisTo.setEndTime(session.getEndTime().getTime());
                        /**
                         * 设置随机码(防止别人恶意来秒杀 seckill?skuId=1&key=?,秒杀的随机码，保护机制,
                         * 带上随机码才能去减信号量代表库存扣减的信息)
                         */
                        redisTo.setRandomCode(randomCode);
                        // 活动id-skuID   秒杀sku信息  场次+skuID   K,V
                        ops.put(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId(), JSON.toJSONString(redisTo));

                        // 信号量可以做分布式限流
                        // 5.使用商品库存(秒杀商品的数量)作为分布式信号量  限流 semaphore信号对象
                        RSemaphore semaphore =
                                redissonClient.getSemaphore(SKU_SKUSTOCK_SEMAPHORE + randomCode);//"seckill:stock:";
                        // 在信号量中设置商品秒杀的数量
                        semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());

                    }
                });

            });
        }
    }

}
